import math

# Convenience attribute
math.twopi = math.pi * 2.0



class Direction(object):
    dir = 0
    instances = {'n': None, 'ne': None, 'e': None, 'se':None, 's':None, 
    'sw':None, 'w':None, 'nw':None}
    cpMap = {'n': 2, 'ne': 1, 'e': 0, 'se':7, 's':6, 'sw':5, 'w':4, 'nw':3}
    intMap = dict([(v, k) for k, v in cpMap.items()])
    
    angle = property(lambda self: self.dir * math.pi / 4)

    def __new__(cls, compassPoint):
        if isinstance(compassPoint, int):
            compassPoint = cls.intMap[compassPoint % 8]
        compassPoint = compassPoint.lower()
        if cls.instances[compassPoint] != None:
            return cls.instances[compassPoint]
        else:
            self = super(cls, Direction).__new__(cls)
            self.dir = cls.cpMap[compassPoint]
            return self
    
    def __str__(self):
        return self.intMap[self.dir]

    def __repr__(self):
        return 'directions.%s' % self
    
    def __neg__(self):
        return Direction(self.intMap[(self.dir + 4) % 8])
    
    def __abs__(self):
        return self
    
    #Kinda superficial, but...
    
    def __int__(self):
        return self.dir
    
    def __add__(self, other):
        if isinstance(other, (Direction,ArbitraryDirection)):
            return ArbitraryDirection(radians=self.angle + other.angle)
        else:
            return Direction((self.dir + other) % 8 )
    
    def __radd__(self, other):
        if isinstance(other, (Direction,ArbitraryDirection)):
            return ArbitraryDirection(radians=self.angle + other.angle)
        else:
            return Direction((self.dir + other) % 8 )

    def __sub__(self, other):
        if isinstance(other, (Direction,ArbitraryDirection)):
            return ArbitraryDirection(radians=self.angle - other.angle)
        else:
            return Direction((self.dir - other + 8) % 8 )
        
    def __rsub__(self, other):
        if isinstance(other, (Direction,ArbitraryDirection)):
            return ArbitraryDirection(radians=other.angle - self.angle)
        else:
            return Direction((self.dir - other + 8) % 8 )
    
    def __eq__(self, other):
        if isinstance(other, Direction):
            return self.dir == other.dir
        elif isinstance(other, ArbitraryDirection):
            return self.angle == other.angle
        else:
            return self.angle == other
    
    def __hash__(self):
        #FIXME
        return int(self)


    
class ArbitraryDirection(Direction):
    angle = None
    
    def __new__(cls, degrees=0.0, radians=0.0):
        
        self = object.__new__(cls)

        if radians == 0.0:
            if degrees / 45.0 in range(8):
                return Direction(int(degrees // 45))
            if degrees > 360:
                degrees = degrees % 360
            if degrees < 0:
                degrees = degrees + 360
            self.dir = int((degrees + 22.5) // 45) % 8
            self.angle = math.radians(degrees)
        else:
            if radians > math.twopi:
                radians %= math.twopi
            if radians < 0:
                radians += math.twopi
            self.dir = int(8 * (radians + math.pi / 16) / math.twopi) % 8
            self.angle = radians

        return self
    
    def __str__(self):
        return "%s (%0.1f)" % (Direction.intMap[self.dir], self.angle)
    
    def __repr__(self):
        return "ArbitraryDirection(radians=%0.5f)" % self.angle
    
    def __hash__(self):
        #FIXME
        return int(self)




class Angle(object):
    # We should probably have this anyway.
    
    DEGREES = 'd'
    RADIANS = 'r'
    UNITS = (DEGREES, RADIANS)
    
    value = 0.0
    unit = DEGREES
    
    def __new__(cls, value, unit=DEGREES):
        if isinstance(value, basestring):
            if value[-1] in cls.UNITS:
                value, unit = float(value[:-1]), value[-1]
            else:
                value, unit = 0.0, degrees
        self = super(Angle, cls).__new__(cls)
        self.angle = value
        self.unit = cls.DEGREES
        if unit in cls.UNITS:
            self.unit = unit
        # Auto-normalize
        #Can't; normalize returns a new Angle(), causing recursion.
        #self.normalize()
        return self
    
    def _toOurUnit(self, unit, value):
        othValue = value
        if unit != self.unit and self.unit == self.DEGREES:
            othValue = math.degrees(othValue)
        elif unit != self.unit and self.unit == self.RADIANS:
            othValue = math.radians(othValue)
        return othValue

    def __add__(self, other):
        if isinstance(other, Angle):
            othValue = self._toOurUnit(other.unit, other.value)
        else:
            othValue = other
        return Angle(self.value+othValue, self.unit)
    
    def __mul__(self, other):
        if isinstance(other, Angle):
            othValue = self._toOurUnit(other.unit, other.value)
        else:
            othValue = other
        return Angle(self.value*othValue, self.unit)
    
    __radd__ = __add__
    __rmul__ = __mul__
    
    def __neg__(self):
        return Angle(-self.value, self.unit)
    
    def __pos__(self):
        return Angle(+self.value, self.unit)
    
    def __sub__(self, other):
        return self + -other
    
    def __rsub__(self, other):
        return -self + other
    
    def __div__(self, other):
        if isinstance(other, Angle):
            othValue = self._toOurUnit(other.unit, other.value)
        else:
            othValue = other
        return Angle(self.value/othValue, self.unit)
    
    def __rdiv__(self, other):
        if isinstance(other, Angle):
            othValue = self._toOurUnit(other.unit, other.value)
        else:
            othValue = other
        return Angle(othValue/self.value, self.unit)
    
    def degrees(self):
        if self.unit == self.DEGREES:
            return self
        else:
            return Angle(math.degrees(self.value), self.DEGREES)
    
    def radians(self):
        if self.unit == self.RADIANS:
            return self
        else:
            return Angle(math.radians(self.value), self.RADIANS)
    
    def normalize(self):
        """Adjusts angle so it is in 0degree to 360degree range"""
        value = self.value
        maxAngle = self.maxAngle
        while value < 0:
            value += maxAngle
        while value > maxAngle:
            value -= maxAngle
    
        return Angle(value, self.unit)
    
    def maxAngle(self):
        if self.unit == self.DEGREES:
            return 360.0
        else:
            return math.twopi



n = N = north = North = ArbitraryDirection(degrees=90)
s = S = south = South = ArbitraryDirection(degrees=270)
e = E = east = East = ArbitraryDirection(degrees=0)
w = W = west = West = ArbitraryDirection(degrees=180)
ne = NE = northeast = Northeast = NorthEast = ArbitraryDirection(degrees=45)
nw = NW = northwest = Northwest = NorthWest = ArbitraryDirection(degrees=135)
se = SE = southeast = Southeast = SouthEast = ArbitraryDirection(degrees=315)
sw = SW = southwest = Southwest = SouthWest = ArbitraryDirection(degrees=225)
compass = [n, ne, e, se, s, sw, w, nw]

if __name__ == "__main__":
    # Peform some tests.
    
    adB = ArbitraryDirection(radians = math.pi)
    
    n = 0.0
    while n < 370:
        adA = ArbitraryDirection(degrees=n)
        print "%15s" % adA, "\t", int(adA), "\t", int(adA - adB)
        n += 11.25
